#!/usr/bin/env python

'''
Program to allow secure ripping using CD paranoia, in the spirit of
EAC and rubyripper.

An important property that I want it to be a drop in replacement
to "cdparanoia [options] <output file>", that is probably used by many
rippers, in particular it has to be compatible with grip and jack.

Note that it assumes that the last argument is the name of a output
sound file.

I am licensing this file under the same licence cdparanoia has.

COPYRIGHT: Paulo Jose da Silva e Silva, email: pssilva at ime dot usp dot br

History:

2nd April 2002. First public release
2nd April 2002. Small fixes from R. Bernstein
'''

# TODO: Better deal with options, letting some options define the
# constants below (and pass the other to cdparanoia)

import sys, md5, os, copy

# Length of a CD-ROM sector, in bytes
SECTORLEN = 2352

# Size of a chunk. I am using the size of a CD-ROM sector
CHUNKSIZE = SECTORLEN

# Number of times the chunks must match. It must be >= 2 to make sense
MINMATCHES = 2

# Number of bytes per second in a PCM (used to compute the position of
# of a possibly bad chunk in seconds)
BYTESPERSECOND = SECTORLEN*75

# Number of times the program should try ripping the track.
NUMBEROFTRIALS = 10

class Chunk(object):
    def __init__(self, pos, digest):
        self.pos = pos
        self.digest = digest
        self.rep = MINMATCHES
        self.saved = True

class Track(object):

    def __init__(self, maxTrials, cdparanoiaOptions, output):
        self.maxTrials = max(2, maxTrials)
        self.cdparanoiaOptions = cdparanoiaOptions
        self.output = output
        self.chunks = []
        self.badChunks = []

    def _callParanoia(self, output):
        retCode = os.system("cdparanoia %s \"%s\"" %
                            (self.cdparanoiaOptions, output))
        if retCode != 0:
            sys.exit(retCode)

    def _initializeChunkList(self):
        pos = 0
        originalData = file(self.output)
        chunk = originalData.read(CHUNKSIZE)
        while chunk != '':
            m = md5.new()
            m.update(chunk)
            self.chunks.append(Chunk(pos, m.digest()))
            pos += 1
            chunk = originalData.read(CHUNKSIZE)
        originalData.close()
            
    def _updateChunks(self, otherChunk):
         
        m = md5.new()
        m.update(otherChunk)
        otherDigest = m.digest()
        chunk = self.chunks.pop(0)
        if otherDigest == chunk.digest:
            if chunk.rep <= 2:
                if not chunk.saved:
                    originalData = os.open(self.output, os.O_WRONLY)
                    os.lseek(originalData, chunk.pos*CHUNKSIZE, 0)
                    os.write(originalData, otherChunk)
                    os.close(originalData)
            else:
                chunk.rep -= 1
                self.chunks.append(chunk)
            
        else:
            chunk.digest = otherDigest
            chunk.rep = MINMATCHES
            chunk.saved = False
            self.chunks.append(chunk)

    def _saveBadChunks(self):
        self.badChunks = self.chunks[:]

    def _problemsLog(self):

        log = file(self.output + '-rip.log', 'w')
        for c in self.badChunks:
            log.write('Original Difference at chunk %d (%ds)\n' %
                      (CHUNKSIZE*c.pos, CHUNKSIZE*c.pos/BYTESPERSECOND))
        for c in self.chunks:
            log.write('*Uncorrected* chunk at %d (%ds)\n' %
                      (CHUNKSIZE*c.pos, CHUNKSIZE*c.pos/BYTESPERSECOND))
        log.close()

    def rip(self):

        # Rip for the first time
        nTrials = 1
        self._callParanoia(self.output)
        self._initializeChunkList()

        while len(self.chunks) > 0 and nTrials < self.maxTrials:
            print '# Chunks to go:', len(self.chunks), '- Trial', nTrials + 1
            tempName = self.output + '.other'
            self._callParanoia(tempName)

            pos = 0
            other = file(tempName)
            chunk = other.read(CHUNKSIZE)
            while len(self.chunks) > 0 and chunk != '':
                if pos == self.chunks[0].pos:
                    self._updateChunks(chunk)
                pos += 1
                chunk = other.read(CHUNKSIZE)

            nTrials += 1
            os.unlink(tempName)
            if nTrials == MINMATCHES: self._saveBadChunks()

        # Create a list of bad chunks
        if len(self.badChunks) > 0:
            self._problemsLog()
        if len(self.chunks) > 0:
            print '***************** BAD RIP *****************'

if __name__ == '__main__':
    cdparanoiaOptions, output = ' '.join(sys.argv[1:-1]), sys.argv[-1]
    t = Track(NUMBEROFTRIALS, cdparanoiaOptions, output)
    t.rip()
